/**
 * 自动部署 v4.0
 * 2025年6月25日12点26分
 */

const { exec } = require('node:child_process')
const path = require('node:path')
const { NodeSSH } = require('node-ssh')
const fs = require('fs-extra')

const inquirer = require('inquirer')
const dayjs = require('../common/dayjs')
const { Option: CommanderOption, program } = require('commander')
const cliProgress = require('cli-progress')
const defineConfig = require('./define-config')

const { getSourceInfo, zipSource } = require('../common/fs')

/** ---- 执行入口 ---- */
if (require.main === module) {
    // 启动
    const AppConfig = require('../config').autoDeploy
    main(AppConfig)
}

/** 入口函数 */
async function main(AppConfig) {
    const rootPath = AppConfig.root_path
    if (!rootPath) {
        throw new Error('请在配置文件中设置 root_path 项，指向项目根目录')
    }

    const apps = AppConfig.apps
    if (!apps || Object.keys(apps).length === 0) {
        throw new Error('请在配置文件中设置 apps 项，指向要部署的应用')
    }

    // 读取配置
    const appKeys = Object.keys(apps)

    const prompt = inquirer.createPromptModule()

    // 读取选择的应用
    const appName =
        appKeys.length > 1
            ? (
                await prompt([
                    {
                        choices: appKeys,
                        message: '选择一个项目',
                        name: 'choice',
                        type: 'list',
                    },
                ])
            ).choice
            : appKeys[0]

    const config = apps[appName]
    const configEnvKeys = Object.keys(config)
    const configEnvChoices = ['*all'].concat(configEnvKeys)

    // 解析参数
    program
        .name('node deploy.js')
        .description('自动部署工具，用于自动打包、上传服务器、执行远程命令等')
        .option('--no-alert', '不打印打包过程中的输出')
        .parse(process.argv)

    const options = program.opts()

    // 如果cmd中带了env参数
    const envName =
        configEnvKeys.length > 1
            ? (
                await prompt([
                    {
                        choices: configEnvChoices,
                        message: '选择要部署的环境',
                        name: 'choice',
                        type: 'list',
                    },
                ])
            ).choice
            : configEnvKeys[0]

    if (envName == '*all') {
        // 多个环境
        console.log(`[部署] 多个环境: ${appName} - ${configEnvKeys.join(', ')}`)
        for (let env of configEnvKeys) {
            console.log(`[部署] 开始: ${appName} - ${env}`)
            await autoDeploy(rootPath, defineConfig(appName, config[env]), options.alert)
        }
        console.log(`[部署] 完成 ${appName} - ${configEnvKeys.join(', ')}`)
    } else {
        // 选择单个
        console.log(`[部署] 开始: ${appName} - ${envName}`)
        await autoDeploy(rootPath, defineConfig(appName, config[envName]), options.alert)
        console.log(`[部署] 完成 ${appName} - ${envName}`)
    }
}

/**
 * 自动部署核心流程
 * @param {string} rootPath 项目根目录
 * @param {object} envConfig 环境配置(某个应用配置中的环境配置)
 * @param {boolean} isAlert 是否打印打包过程中的输出，默认 true
 */
async function autoDeploy(rootPath, envConfig, isAlert = true) {
    // ---- 单独执行远程指令 ----
    if (envConfig.remote_cmd) {
        console.log(`[单独执行远程指令]`)
        const config = envConfig.remote_cmd
        const ssh = new NodeSSH()
        try {
            await ssh.connect(envConfig.server)
            await shellExecCmd(
                ssh,
                config.cmd,
                {
                    cwd: config.cwd || envConfig.remote_dir,
                },
                isAlert
            )
        } catch (error) {
            console.error('SSH执行失败:', error)
        } finally {
            ssh.dispose()
        }
        return
    }

    // 开始计时
    const time = getDisplayTime(new Date())
    console.log(`[--------------------] 开始任务流 ${time.start()}`)

    // ---- 执行打包 ----
    if (envConfig.build) {
        const config = envConfig.build
        console.log(`[**------------------] 开始打包 (${config.cmd}) [${time.next()}ms]`)

        await new Promise((resolve, reject) => {
            const cp = exec(config.cmd, { cwd: config.cwd || rootPath }, (err, stdout, stderr) => {
                if (err) {
                    console.error(`[!!!] 执行打包出错: ${err}`)
                    return reject(err)
                }
                console.log(`[******--------------] 打包完成 [${time.next()}ms]`)
                resolve(true)
            })
            if (isAlert) cp.stdout.on('data', (data) => process.stdout.write(data))
        })
    }

    // ---- 计算被上传的资源 ----
    if (!envConfig.source || !envConfig.source.length) {
        throw new Error('请在配置文件中设置 source 项，指向要上传的资源')
    }
    const sourceInfo = getSourceInfo(envConfig.source)

    if (sourceInfo.source.length === 0) {
        throw new Error(`匹配到的上传资源为空，请检查source配置: ${envConfig.source}`)
    }

    /** ---- 是否要压缩 ---- */
    const zipFilePathname = path.resolve(envConfig.zip?.dir || rootPath, envConfig.zip?.file_name || 'dist-temp.zip')
    if (envConfig.zip) {
        console.log(`[******--------------] 开始压缩 [${time.next()}ms]`)
        await zipSource(sourceInfo, zipFilePathname)
        console.log(`[**********----------] 压缩完成 [${time.next()}ms]`)
    }

    /** ---- 执行上传 ---- */
    if (!envConfig.server || !envConfig.server.host) {
        throw new Error('请在配置文件中设置 server 项，指向服务器信息')
    }
    if (!envConfig.remote_dir) {
        throw new Error('请在配置文件中设置 remote_dir 项，指向服务器部署目录')
    }

    const ssh = new NodeSSH()
    await ssh.connect(envConfig.server).catch((err) => {
        console.error(`[!!!] 连接远程服务器出错:`)
        console.error(err)
    })
    console.log(`[**************------] 连接服务器成功 ${envConfig.server.host} [${time.next()}ms]`)

    const remoteDir = path.posix.normalize(envConfig.remote_dir.replace(/\/$/, '')) // 确保目录不以/结尾
    if (envConfig.zip) {
        // 如果有压缩包，上传压缩包
        const removeZipFilename = path.posix.join(remoteDir, path.basename(zipFilePathname))
        console.log(
            `[**************------] 开始上传压缩包 ${zipFilePathname} -> ${removeZipFilename} [${time.next()}ms]`
        )
        await ssh.putFile(zipFilePathname, removeZipFilename)
        console.log(`[******************--] 压缩包上传完成 [${time.next()}ms]`)

        // 解压缩
        console.log(`[******************--] 服务器解压文件 ${removeZipFilename} 到 ${remoteDir} [${time.next()}ms]`)
        // 服务器是/分隔符，确保removeZipFilename路径正确
        await shellExecCmd(
            ssh,
            `unzip -o ${removeZipFilename} -d ${remoteDir}`,
            {
                cwd: remoteDir,
            },
            false
        )

        // 删除本地压缩包
        console.log(`[******************--] 删除本地压缩包 ${zipFilePathname} [${time.next()}ms]`)
        fs.removeSync(zipFilePathname)

        // 删除远程压缩包
        console.log(`[*******************-] 删除远程压缩包 ${removeZipFilename} [${time.next()}ms]`)
        await shellExecCmd(
            ssh,
            `rm -f ${removeZipFilename}`,
            {
                cwd: remoteDir,
            },
            false
        )
    } else {
        // 不用压缩功能，直接上传sourceInfo
        const bar = new cliProgress.SingleBar({
            format: '[{bar}] {percentage}% | {value}/{total} 文件',
            barCompleteChar: '=',
            barIncompleteChar: ' ',
            hideCursor: true,
        })
        console.log(`[**************------] 开始上传资源 ${sourceInfo.source.length} 个文件 [${time.next()}ms]`)
        bar.start(sourceInfo.source.length, 0)
        for (const [idx, filePath] of sourceInfo.source.entries()) {
            const remoteFilePath = path.posix.join(remoteDir, path.relative(sourceInfo.base_dir, filePath))
            await ssh.putFile(filePath, remoteFilePath)
            bar.update(idx + 1)
        }
        bar.stop()
    }

    // 执行完毕
    ssh.dispose()
    console.log(`[********************] 部署完成 [${time.next()}ms]`)
}

/**
 * 服务器执行指令
 * @param {NodeSSH} ssh 连接实例
 * @param {string|string[]} cmd 指令，可以是单条指令或多条指令的数组
 * @param {object} options 执行选项，默认 {}
 * @param {boolean} showStdout 是否打印回执
 */
async function shellExecCmd(ssh, cmd, options = {}, showStdout = false) {
    // 如果是数组，用 && 连接成单条命令
    const command = Array.isArray(cmd) ? cmd.join(' && ') : cmd

    const execOptions = {
        cwd: options.cwd || undefined,
        onStdout: showStdout ? (chunk) => process.stdout.write(chunk) : undefined,
        onStderr: showStdout ? (chunk) => process.stderr.write(chunk) : undefined,
        ...options,
    }
    const result = await ssh.execCommand(command, execOptions)
    if (showStdout && result.stdout) process.stdout.write(result.stdout)
    if (result.code !== 0) {
        throw new Error(`远程命令执行失败: ${command}\n${result.stderr}`)
    }
    return result
}

/** 记录耗时 */
function getDisplayTime(date) {
    let cur = date
    return {
        done() {
            const now = new Date()
            return now.valueOf() - date.valueOf()
        },
        next() {
            const now = new Date()
            const diff = now.valueOf() - cur.valueOf()
            cur = now
            return diff
        },
        start() {
            // 使用 dayjs 格式化时间
            return `${dayjs(date).format('HH:mm:ss SSS')}`
        },
    }
}
